import { Scene } from '@babylonjs/core/scene';
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { Matrix, Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Mesh } from '@babylonjs/core/Meshes/mesh';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { CanvasTextureText, ICanvasTextureTextOptions } from '../texture/canvas-texture-text';
import { StripsLineGeometry } from '../geometry/strips-line-geometry';
import { StripsLineMaterial } from '../material/strips-line-material';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
import { BasicLineGeometry } from '../geometry/basic-line-geometry';
import { BasicLineMaterial } from '../material/basic-line-material';
import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
import { AnimationFrame, ITimer } from '../animation/animation-frame';


/**
 * 标签参数对象
 * @param size 尺寸大小
 * @param billboardMode 标签朝向模式
 * @param startOffset 起始端偏移
 * @param pointLineWidth 连线宽度
 * @param pointLineColor 连线颜色
 * @param autoUpdatePointLineStart 是否自动更新连线起始端位置
 * @param pointRadius 端点半径
 * @param useRegularMesh 是否使用普通网格（否则端点为实例网格）
 * @param useBasicLine 是否使用基础线
 * @param autoUpdateInterval 自动更新间隔
 */
interface IPointingLabelOptions extends ICanvasTextureTextOptions {
    size?: number,
    billboardMode?: number,
    startOffset?: number,
    pointLineWidth?: number,
    pointLineColor?: Color3,
    autoUpdatePointLineStart?: boolean,
    pointRadius?: number,
    useRegularMesh?: boolean,
    useBasicLine?: boolean,
    autoUpdateInterval?: number
}

/**
 * 标签样式
 * @param size 尺寸大小
 * @param pointLineWidth 连线宽度
 * @param pointLineColor 连线颜色
 * @param pointRadius 端点半径
 */
interface IPointingLabelStyle extends ICanvasTextureTextOptions {
    size?: number,
    pointLineWidth?: number,
    pointLineColor?: Color3,
    pointRadius?: number,
}


/**
 * 标签
 * @param name 名称
 * @param options 参数对象
 * @param scene 所在场景
 */
class PointingLabel extends TransformNode {

    private _label!: Mesh;
    private _line: Mesh | null = null;
    private _spot: Mesh | null = null;
    private _ims: Array<InstancedMesh | Mesh> = [];
    private _points: Vector3[] = [];
    private _linePoints: Vector3[] = [];
    private _tm0: Matrix = new Matrix();
    private _width!: number;
    private _height!: number;
    private _corners: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3()];
    private _tq0: Quaternion = new Quaternion();
    private _tq1: Quaternion = new Quaternion();
    private _autoUpdateLineStartHandle!: { (): void };
    private _insScaling: Vector3 = new Vector3(1, 1, 1);
    private _req: ITimer = {};

    private _options: IPointingLabelOptions;

    constructor(name: string, options?: IPointingLabelOptions, scene?: Scene) {
        super(name, scene);
        const ops = options || {};
        this._options = ops;
        const ctt = this.createTexture();
        this._label = this.createLabelPlane(ctt.width, ctt.height);
        const material = this._label.material as StandardMaterial;
        material.disableLighting = true;
        material.emissiveTexture = ctt.texture;
        material.opacityTexture = ctt.texture;
        this.rotationQuaternion = Quaternion.Identity();
    }

    /**
     * 获取标签平面
     */
    public get label(): Mesh {
        return this._label;
    }

    /**
     * 获取连线
     */
    public get line(): Mesh | null {
        return this._line;
    }

    /**
     * 设置是否显示
     */
    public set display(display: boolean) {
        this._label.isVisible = display;
        if (this._line) this._line.isVisible = display;
        this._ims.forEach(im => im.isVisible = display);
    }

    /**
     * 设置目标点（标签连线指向的位置）
     */
    public set points(points: Vector3[]) {
        if (points.length < 1) {
            if (this._line) {
                this._line.parent = null;
                this._line.dispose();
                this._line = null;
                this._spot!.dispose();
                this._spot = null;
                this._ims.forEach(im => {
                    im.setParent(null);
                    im.dispose();
                });
                this._ims.length = 0;
            }
            return;
        }
        const scn = this.getScene();
        const ops = this._options;
        const color = ops.pointLineColor || new Color3(1, 1, 1);
        const pointLineWidth = ops.pointLineWidth || 0.01;
        const radius = ops.pointRadius || 0.1;
        const name = this.name;

        const ps = this._points;
        ps.length = 0;
        const inv = this._tm0.copyFrom(this.getWorldMatrix()).invert();
        for (const point of points) {
            ps.push(Vector3.TransformCoordinatesToRef(point, inv, point));
        }
        const lps = this._linePoints;
        lps.length = 0;
        for (const p of ps) {
            lps.push(new Vector3(0, 0, 0), p);
        }
        if (this._line) {
            if (this._options.useBasicLine) {
                (this._line.geometry as BasicLineGeometry).vertices = lps;
                const material = this._line.material as BasicLineMaterial;
                material.color = color;
            } else {
                (this._line.geometry as StripsLineGeometry).vertices = lps;
                const material = this._line.material as StripsLineMaterial;
                material.width = pointLineWidth;
                material.diffuseColor = color;
                material.backFaceCulling = false;
            }

            const scl = this._insScaling.set(radius, radius, radius);
            (this._spot!.material as StandardMaterial).emissiveColor = color;
            for (let i = 0; i < lps.length; i++) {
                this._ims[i].position = lps[i];
                this._ims[i].scaling = scl;
            }
        } else {
            if (this._options.useBasicLine) {
                const g = new BasicLineGeometry(name + '-line-geometry', lps, {}, scn);
                const m = new BasicLineMaterial(name + '-line-material', scn, {
                    color: color,
                    fillMode: ShaderMaterial.LineListDrawMode,
                });
                this._line = new Mesh(name + '-line-mesh', scn);
                g.applyToMesh(this._line);
                this._line.material = m;
            } else {
                const g = new StripsLineGeometry(name + '-line-geometry', lps, {}, scn);
                const m = new StripsLineMaterial(name + '-line-material', scn, {
                    width: pointLineWidth,
                    diffuseColor: color
                });
                m.backFaceCulling = false;
                this._line = new Mesh(name + '-line-mesh', scn);
                g.applyToMesh(this._line);
                this._line.material = m;
            }

            this._line.parent = this;
            this._line.alwaysSelectAsActiveMesh = true;

            this._spot = MeshBuilder.CreateIcoSphere(name + '-from-spot-mesh', {
                radius: 1,
                subdivisions: 2
            }, scn);
            const mf = new StandardMaterial(name + '-from-spot-material', scn);
            mf.disableLighting = true;
            mf.emissiveColor = color;
            const scl = this._insScaling.set(radius, radius, radius);
            this._spot.material = mf;
            this._spot.isVisible = false;
            this._ims.length = 0;
            for (let i = 0; i < lps.length; i++) {
                const im = this._options.useRegularMesh ?
                    this._spot.clone(name + '-from-spot' + i) :
                    this._spot.createInstance(name + '-from-spot-instance' + i);
                im.alwaysSelectAsActiveMesh = true;
                im.setParent(this);
                im.position = lps[i];
                im.scaling = scl;
                im.isVisible = true;
                this._ims.push(im);
            }
        }
    }

    /**
     * （动态）设置标签样式
     */
    public set style(style: IPointingLabelStyle) {
        const ops = this._options;
        let labelNeedsUpdate = false;
        let lineStartNeedsUpdate = false;
        let lineNeedsUpdate = false;
        let spotNeedsUpdate = false;
        for (let key in style) {
            switch (key) {
                case 'text':
                    ops.text = style.text;
                    labelNeedsUpdate = true;
                    break;
                case 'size':
                    ops.size = style.size;
                    labelNeedsUpdate = true;
                    lineStartNeedsUpdate = true;
                    break;
                case 'pointLineWidth':
                    ops.pointLineWidth = style.pointLineWidth;
                    lineNeedsUpdate = true;
                    break;
                case 'pointLineColor':
                    ops.pointLineColor = style.pointLineColor;
                    lineNeedsUpdate = true;
                    break;
                case 'pointRadius':
                    ops.pointRadius = style.pointRadius;
                    spotNeedsUpdate = true;
                    break;
            }
        }
        if (labelNeedsUpdate) this.updateLabel();
        if (lineStartNeedsUpdate) this.updateLineStart();
        if (lineNeedsUpdate) this.updateLineStyle();
        if (spotNeedsUpdate) this.updateSpotStyle();
    }

    /**
     * 更新连线起始端
     */
    public updateLineStart(): void {
        if (!this._line) return;
        const label = this._label;
        const cnr = this._corners;
        const lps = this._linePoints;
        const rot = this._tq0.copyFrom(label.absoluteRotationQuaternion);
        const inv = this._tq1.copyFrom(this.absoluteRotationQuaternion);
        inv.invertInPlace();
        this.setCorner();
        cnr.forEach(p => {
            p.applyRotationQuaternionInPlace(rot);
            p.applyRotationQuaternionInPlace(inv);
        });
        for (let i = 1; i < lps.length; i += 2) {
            const sp = lps[i];
            this.closestPointInPoints(sp, cnr, lps[i - 1]);
        }

        if (this._options.useBasicLine) {
            (this._line!.geometry as BasicLineGeometry).vertices = lps;
        } else {
            (this._line!.geometry as StripsLineGeometry).vertices = lps;
        }

        const ins = this._ims;
        for (let i = 0; i < lps.length; i++) {
            ins[i].position = lps[i];
        }
    }

    /**
     * 销毁
     * @param doNotRecurse 不递归销毁子级对象
     * @param disposeMaterialAndTextures 销毁材质和纹理
     */
    public dispose(doNotRecurse?: boolean | undefined, disposeMaterialAndTextures?: boolean | undefined): void {
        super.dispose(doNotRecurse, disposeMaterialAndTextures);
        AnimationFrame.cancel(this._req);
    }

    /**
     * 更新端点样式
     */
    private updateSpotStyle(): void {
        if (!this._line) return;
        const ops = this._options;
        const radius = ops.pointRadius || 0.1;
        const scl = this._insScaling.set(radius, radius, radius);
        for (let i = 0; i < this._ims.length; i++) {
            this._ims[i].scaling = scl;
        }
    }

    /**
     * 更新连线样式
     */
    private updateLineStyle(): void {
        if (!this._line) return;
        const ops = this._options;
        const color = ops.pointLineColor || new Color3(1, 1, 1);
        if (this._options.useBasicLine) {
            const material = this._line.material as BasicLineMaterial;
            material.color = color;
        } else {
            const material = this._line.material as StripsLineMaterial;
            material.width = ops.pointLineWidth || 0.01;
            material.diffuseColor = color;
        }

        (this._spot!.material as StandardMaterial).emissiveColor = color;
    }

    /**
     * 更新标签
     */
    private updateLabel(): void {
        const ops = this._options;
        const ctt = this.createTexture();
        const size = ops.size || 1;
        this._width = ctt.width * size;
        this._height = ctt.height * size;
        this._label.scaling.set(this._width, this._height, 1);
        this._label.billboardMode = ops.billboardMode !== undefined ? ops.billboardMode : TransformNode.BILLBOARDMODE_ALL;
        const material = this._label.material as StandardMaterial;
        material.emissiveTexture?.dispose();
        material.opacityTexture?.dispose();
        material.emissiveTexture = ctt.texture;
        material.opacityTexture = ctt.texture;
        this.updateLineStart();
    }

    /**
     * 点集中与目标点最近的点
     * @param point 目标点
     * @param points 点集
     * @param out 最近点
     */
    private closestPointInPoints(point: Vector3, points: Vector3[], out: Vector3): void {
        let dis = Number.POSITIVE_INFINITY;
        let mi = 0;
        for (let i = 0; i < points.length; i++) {
            let d = Vector3.DistanceSquared(point, points[i]);
            if (d < dis) {
                dis = d;
                mi = i;
            }
        }
        out.copyFrom(points[mi]);
    }

    /**
     * 创建内容纹理
     * @returns 纹理
     */
    private createTexture(): CanvasTextureText {
        return new CanvasTextureText(this.name + '-label-material-texture', this._options, this.getScene());
    }

    /**
     * 创建标签网格平面
     * @param width 宽
     * @param height 高
     * @returns 网格平面
     */
    private createLabelPlane(width: number, height: number): Mesh {
        const name = this.name;
        const size = this._options.size || 1;
        const w = this._width = width * size;
        const h = this._height = height * size;
        const scene = this.getScene();
        const plane = MeshBuilder.CreatePlane(name + '-label-mesh', { size: 1 }, scene);
        plane.scaling.set(w, h, 1);
        plane.billboardMode = this._options.billboardMode !== undefined ? this._options.billboardMode : TransformNode.BILLBOARDMODE_ALL;
        const material = new StandardMaterial(name + '-label-material', scene);
        plane.material = material;
        plane.parent = this;
        this._autoUpdateLineStartHandle = () => { this.updateLineStart() };

        if (this._options.autoUpdatePointLineStart) {
            const t = this._options.autoUpdateInterval || 500;
            AnimationFrame.request(() => {
                this._autoUpdateLineStartHandle();
            }, t, t + 200, this._req);
        } else {
            AnimationFrame.throttleByFrame(this._req, 1, () => {
                this._autoUpdateLineStartHandle();
            }, false);
        }

        return plane;
    }

    /**
     * 设置标签的角点（左上、上、右上...共8个，作为可用连线起点）
     */
    private setCorner(): void {
        const cnr = this._corners;
        const startOffset = this._options.startOffset || 0;
        const w = this._width * 0.5 + startOffset;
        const h = this._height * 0.5 + startOffset;
        cnr[0].set(w, h, 0);
        cnr[1].set(-w, h, 0);
        cnr[2].set(w, -h, 0);
        cnr[3].set(-w, -h, 0);
        cnr[4].set(0, h, 0);
        cnr[5].set(0, -h, 0);
        cnr[6].set(-w, 0, 0);
        cnr[7].set(w, 0, 0);
    }
}


export { PointingLabel };
export type { IPointingLabelOptions, IPointingLabelStyle };